package tv.geir.slackspace.matching;

import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * Use this class to compare two instances of <code>tv.geir.slackspace.Template</code>.
 * 
 * For one template (search) to "match" another template (instance), the search template
 * can only contain information that also exist and is equally the same as in the instance. 
 * <p>
 * A template can both be from another object or by setting it up manually. The following
 * example illustrates how the template <code>instance</code> is created from an existing
 * object. Another template, <code>search</code> is then created and properties are set
 * manually. <code>search</code> is not equal to <code>instance</code>, but all information
 * in <code>search</code> is also to be found in <code>instance</code>. In other words 
 * <code>search</code> matches <code>instance</code>, or like the API: 
 * <code>instance</code> is assignable from <code>search</code> 
 * 
 *  <pre>
 *  	myObject = new MyObject();
 *  	myObject.setText( "Hello world" );
 *  	myObject.setCount( 1 );
 *  
 *  	// create exact template from object
 *  	Template instance = new Template( myObject, true );	
 *  	
 *  	// create a search template
 *  	Template search = new Template();
 *  	search.addBeanProperty( "text", "java.lang.String", "Hello world" );
 *  
 *  	// test if searh matches instance
 *  	TemplateMatcher matcher = new TemplateMatcher();
 *  	boolean match = matcher.isAssignableFrom( instance, search ); *  	
 *  </pre>
 *  <code>match</code> will in this example be <code>true</code> because <code>search</code>
 *  matches <code>instance</code>.
 *  <p>
 *  TODO MATCH ON SUPERCLASS/INTERFACES
 *  FIXME REWRITE THIS:: (wrong info)
 *  IMPORTANT: If your template represent objects that contain primitive datatypes (int, short, char, long, etc.)
 *  the value of this bean's properties or public fields will not be considered during matching
 *  if the value of the primitive used for searching is default value for its type. This is because
 *  primitives does not allow null-values and wildcard search for primitives had to be implemented one way.
 *  May be subject to change in later version (perhaps add a flag which has to be set 
 *  when searching using primitives with default values).
 *  
 * @author Geir Gullestad Pettersen
 *
 */
public class TemplateMatcher {

	private static Log log = LogFactory.getLog( TemplateMatcher.class );
	
    private ValueComparator valueComparator = new ValueComparator();
	
	public boolean isAssignableFrom( Template instance, Template searchPattern ) {
		boolean match = true;

		if( !checkId( instance, searchPattern ) ) {
			match = false;
		} else if( match && !checkClassName( instance, searchPattern )) {
			match = false;
		} else if( match && !checkPublicFields( instance, searchPattern )) {
			match = false;
		} else if( match && !checkBeanProperties( instance, searchPattern )) {
			match = false;
		}
			
		// check superclasses and interfaces
		
		return match;
	}
	
	/**
	 * Checks wheter <code>search</code>'s id can be used to identify 
	 * <code>instance</code>. Search id can be a complete match or 
	 * wildcard (<code>null</code>) <p>
	 * <p>
	 * Search matches instance if any of following are true:<ul>
	 * 		<li> instance.id == search.id </li>
	 * 		<li> instance.id != null && search.id == null </li>
	 * 		</ul><p>
	 * 		fails match if any of the following are true:<ul>
	 * 		<li>instance.id == null</li>
	 * 		<li>any other cases</li></ul><p>
	 * 
	 * @param instance
	 * @param search
	 * @return true if match, false if mismatch.
	 */
	public boolean checkId( Template instance, Template search ) {
		boolean match = false;
		if( search.getId() == null || !search.isForSpecificInstance()) {
			match = true;
		} else if( instance.getId() == null ){
			match = false;
		} else {
			match = instance.getId().equals( search.getId() );
		}
        if( log.isDebugEnabled() ) {
			log.debug( "Match on id: " + match);
        }
        return match;
	}
	
	
	
	@SuppressWarnings("unchecked")
	protected boolean isClassAssignableFrom( String instanceClassName, String searchClassName ) {
		try {
			Class i = Class.forName( instanceClassName );
			Class s = Class.forName( searchClassName );
			
			return s.isAssignableFrom( i );
		} catch ( Throwable t ) {
			log.warn( "Error checking class relationship between search: " +
					"\'" + searchClassName + "\' and instance: \'" + instanceClassName +"\'.", t);
			return false;
		}
	}
	
	/**
	 * Checks wheter the property <code>instance</code> is assignable from
	 * <code>search</code> or not. <p>
	 * <p> 
	 * @param instance
	 * @param search
	 * @return true if <code>search</code> can be used to identify <code>instance</code>
	 */
	public boolean isPropertyAssignableFrom( TemplateProperty instance, TemplateProperty search ) {
		
		if( search.name != null && 
			!search.name.equals( instance.name )) {
			/* No wildcard
			 * Property name mismatch
			 */
			return false;
			
		} else if ( 
				search.type != null &&
				!search.type.equals( instance.type ) &&
				!isClassAssignableFrom( instance.type, search.type )) {
			/* No wildcard
			 * Types are not equal
			 * Instance is not assignable from search (verified by using reflection)
			 */ 
			return false;
			
		} else if ( search.value != null &&
                !valueComparator.evaluate( 
    				instance.getValue(),  
    				search.getOperator(), 
    				search.getValue(),
    				search.isDefaultPrimitiveIsWildcard() )) {
				
			// value didn't match
			return false;
		}
		
		// all tests passed, we've got a match!
		return true;		
	}
	
	
	/**
	 * Checks if <code>search</code> matches any of the properties in <code>propertyList</code>
	 * @param instancePropertyList
	 * @param search
	 * @return <code>true</code> if there is a match
	 */
	public boolean hasMatchingProperty( Set<TemplateProperty> instancePropertyList, TemplateProperty search ) {
		for( TemplateProperty instance : instancePropertyList ) {
			if( isPropertyAssignableFrom( instance, search )) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * 
	 * @param instanceSet
	 * @param searchSet
	 * @return
	 */
	public boolean propertiesMatchSearchPattern( Set<TemplateProperty> instanceSet, Set<TemplateProperty> searchSet ) {
		// loop through search. and check that every search-property matches at least
		// one property in the instance set.
		boolean match = true;
		for( TemplateProperty search : searchSet ) {
			if( !hasMatchingProperty( instanceSet, search )) {
				match = false;
				if( log.isDebugEnabled() ) {
					log.debug( "Mismatch on field/beanproperty search=[" + search + "]" );
				}
				break;
			}
		}
		if( match && log.isDebugEnabled() ) {
			log.debug( "Match all fields/beanproperties" );
		}
		return match;
	}
	/**
	 * @param instance
	 * @param search
	 * @return
	 */
	public boolean checkPublicFields( Template instance, Template search ) {
		return propertiesMatchSearchPattern( instance.getPublicFields(), search.getPublicFields() );
	}
	/**
	 * Checks wheter <code>instance</code>'s bean properties can be matched by <code>search</code>'s
	 * 
	 * @param instance
	 * @param search
	 * @return
	 */
	public boolean checkBeanProperties( Template instance, Template search ) {
		return propertiesMatchSearchPattern( instance.getBeanProperties(), search.getBeanProperties() );
	}
	
	/**
	 * Checks if <code>search</code>'s classname 
	 * @param instance
	 * @param search
	 * @return
	 */
	public boolean checkClassName( Template instance, Template search ) {
		boolean match = false;
		
		// class name
		String sName = search.getName();
		String iName = instance.getName();
		if( sName == null || "".equals( sName )) {
			// wildcard
			match = true;
			if( log.isDebugEnabled() ) {
				log.debug( "Matched classname on wildcard" );
			}
		}
		else if( sName.equals( iName )) {
			// exact match
			match = true;
			if( log.isDebugEnabled() ) {
				log.debug( "Matched classname of instance (exact name match)" );
			}
		} else {
			// check superclasses 
			Set<String> superClasses = instance.getSuperClasses();
			for( String iSuper : superClasses ) {
				if( sName.equals( iSuper )) {
					match = true;
					if( log.isDebugEnabled() ) {
						log.debug( "Matched classname on superclass of instance" );
					}
				}
			}
		}
		
		if( !match && log.isDebugEnabled() ) {			
			log.debug( "Mismatch on classnames: instance=[" + iName + "] search=["+sName+"]" );
		}
		return match;
	}
	
	
	
	
	
	
}
